#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Xml;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using InputEventSystem;
using SkinnedModel;
using MorphTarget;
using SpeechLib;
using ConfigReader;
using Primitives3D;
using XNAViewerSample.Forms;
#endregion

namespace XNAViewerSample
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public partial class XNAViewer : Microsoft.Xna.Framework.Game
    {
        #region Fields
        GraphicsDeviceManager graphics;
        ContentManager worldLoader, mainLoader, tempLoader;

        // Text UI
        private SpriteBatch spriteBatch;
        private SpriteFont spriteFont;
        private SpriteFont playlistFont;
        private String message = "FPS: ";
        private float fps;
        private String debugMsg = "";
        private double accumElapsedTimeSec;
        private int frames;

        // Input system
        private InputEvents input;
        private Control_Panel controlPanel;

        // Game Components
        private Dictionary<String, CameraComponent> camera;
        private ModelsComponent modelManager;
        private SkyboxComponent skybox1;

        // Media
        private List<Song> songList = new List<Song>();

        // Voice
        private SpVoice avatarVoice;
        private ISpeechObjectTokens spToken;

        // Game specific variables
        //private float headRot, headArc;
        //private bool headFollow;
        private Texture2D blank;

        private const int WINDOW_WIDTH = 640;
        private const int WINDOW_HEIGHT = 480;
        private bool _DEBUG_ = false;
        //private SpherePrimitive[] dSphere = new SpherePrimitive[36];
        private String modelHook;
        public enum ResponseType { TEXT, BROWSER, SYSTEM };
        public enum SystemType { COMMAND, MEDIA };

        #endregion

        public XNAViewer()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            graphics.PreferredBackBufferWidth = WINDOW_WIDTH;
            graphics.PreferredBackBufferHeight = WINDOW_HEIGHT;
            graphics.PreferMultiSampling = true;

            //IsFixedTimeStep = false;      <-- need to be TRUE to sync properly with voice
            IsMouseVisible = true;
            this.Window.Title = "Esk is Not an Avatar";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // create content manager loaders
            // worldLoader is for contents that persists throughout the app (e.g. maps)
            worldLoader = new ContentManager(this.Services, "./Content");
            // mainLoader is for contents that are model specific
            mainLoader = new ContentManager(this.Services, "./Content");
            // tempLoader is for contents that are temporary and changes frequently
            tempLoader = new ContentManager(this.Services, "./Content");          
            
            // Add input handlers (keyboard and mouse)
            input = new InputEvents(this);
            this.Components.Add(input);
            input.KeyDown += new KeyDownHandler(KeyDownFunction);
            //mouse is reserved for camera
            //input.MouseMove += new MouseMoveHandler(MouseMoveFunction);
            //input.MouseDown += new MouseDownHandler(MouseDownFunction);
            //input.MouseUp += new MouseUpHandler(MouseUpFunction);
            input.Enabled = true;

            // Create Forms
            controlPanel = new Control_Panel();
            controlPanel.modelLoadHandler += new ModelLoadHandler(Panel_ModelLoadHandler);
            controlPanel.modelClothTypeHandler += new ModelClothTypeHandler(Panel_modelClothTypeHandler);
            controlPanel.modelClothHandler += new ModelClothHandler(Panel_modelClothHandler);
            controlPanel.modelHairTypeHandler += new ModelHairTypeHandler(Panel_modelHairTypeHandler);
            controlPanel.modelAnimationHandler += new ModelAnimationHandler(Panel_modelAnimationHandler);
            controlPanel.modelAccessoryHandler += new ModelAccessoryHandler(Panel_modelAccessoryHandler);
            controlPanel.modelDanceHandler += new ModelDanceHandler(Panel_modelDanceHandler);
            controlPanel.modelMorphHandler += new ModelMorphHandler(Panel_modelMorphHandler);
            controlPanel.modelVisemeHandler += new ModelVisemeHandler(Panel_modelVisemeHandler);

            // Add Camera component/s
            ConfigReader.CameraXML[] cameraXML = worldLoader.Load<ConfigReader.CameraXML[]>("Models/Cameras");
            camera = new Dictionary<string, CameraComponent>(cameraXML.Length);
            foreach (CameraXML entry in cameraXML)
            {
                camera[entry.Name] = new CameraComponent(this);
                this.Components.Add(camera[entry.Name]);
                camera[entry.Name].FoV = entry.FOV;
                if (entry.NearPlane != null)
                    camera[entry.Name].NearPlaneDistance = (float)entry.NearPlane;
                if (entry.FarPlane != null)
                    camera[entry.Name].FarPlaneDistance = (float)entry.FarPlane;
                if (entry.Viewport == null)
                    camera[entry.Name].Viewport = graphics.GraphicsDevice.Viewport;
                else
                {
                    Vector4 vec = new Vector4();
                    vec = (Vector4) entry.Viewport;
                    camera[entry.Name].Viewport = new Viewport
                    {
                        MinDepth = 0,
                        MaxDepth = 1,
                        X = (int)vec.X,
                        Y = (int)vec.Y,
                        Width = (int)vec.Z,
                        Height = (int)vec.W,
                    };
                }
                camera[entry.Name].Controls = input;
                if (entry.Name == "Main")
                    camera[entry.Name].cameraHandler += new CameraHandler(Main_cameraHandler);
                if (entry.Name == "Sub")
                    camera[entry.Name].cameraHandler += new CameraHandler(Sub_cameraHandler);
                camera[entry.Name].ChaseName = entry.Chase;
                camera[entry.Name].DesiredPositionOffset = entry.PosOffset;
                camera[entry.Name].LookAtOffset = entry.LookOffset;
                camera[entry.Name].Enabled = entry.Enable;
            }

            // Add Skybox component
            skybox1 = new SkyboxComponent(this);
            this.Components.Add(skybox1);
            skybox1.ActiveCamera = camera["Main"];
            skybox1.ContentLoader = worldLoader;
            skybox1.Enabled = false;

            // Add Model Manager
            modelManager = new ModelsComponent(this);
            this.Components.Add(modelManager);
            modelManager.MainCamera = camera["Main"];
            modelManager.SubCamera = camera["Sub"];
            modelManager.ContentLoader = mainLoader;
            modelManager.Enabled = true;

            // Setup Voice & Chat
            if ((avatarVoice = new SpVoice()) != null)
            {
                avatarVoice.EventInterests |= SpeechVoiceEvents.SVEViseme;
                avatarVoice.EventInterests |= SpeechVoiceEvents.SVEEndInputStream;
                avatarVoice.Viseme += new _ISpeechVoiceEvents_VisemeEventHandler(avatarVoice_Viseme);
                avatarVoice.EndStream += new _ISpeechVoiceEvents_EndStreamEventHandler(avatarVoice_EndStream);

                spToken = avatarVoice.GetVoices("Gender=Female", "");
                for (int i = 0; i < spToken.Count; i++)
                    controlPanel.AddVoice(spToken.Item(i).GetDescription(0));
                // set to default
                controlPanel.Voice = avatarVoice.Voice.GetDescription(0);
                avatarVoice.Voice = spToken.Item(controlPanel.GetVoiceIndex());
                controlPanel.Volume = avatarVoice.Volume;
                controlPanel.Rate = avatarVoice.Rate;
                // add handler
                controlPanel.voiceOptionHandler += new VoiceOptionHandler(Panel_voiceOptionHandler);
            }
            controlPanel.chatInputHandler += new ChatInputHandler(Panel_chatInputHandler);

            // Game specific initialization
            //headArc = 0.0f;
            //headRot = 0.0f;
            //headFollow = false;

            // Add drag n drop capability for music play
            System.Windows.Forms.Form f = System.Windows.Forms.Form.FromHandle(this.Window.Handle) as System.Windows.Forms.Form;
            f.AllowDrop = true;
            f.DragOver += new System.Windows.Forms.DragEventHandler(f_DragOver);
            f.DragDrop += new System.Windows.Forms.DragEventHandler(f_DragDrop);
            MediaPlayer.MediaStateChanged += new EventHandler<EventArgs>(MediaPlayer_MediaStateChanged);
            MediaPlayer.Stop();

            base.Initialize();

            // show chat form
            Rectangle rect = this.Window.ClientBounds;
            controlPanel.SetDesktopLocation(rect.Left - controlPanel.Width -4, rect.Top -21);
            controlPanel.ControlBox = false;
            controlPanel.Show();
            controlPanel.Focus();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // if (_DEBUG) {
            //for (int i = 0; i < dSphere.Length; i++)
            //    if (dSphere[i] == null) dSphere[i] = new SpherePrimitive(GraphicsDevice, 1, 8);
            //}

            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
            spriteFont = worldLoader.Load<SpriteFont>("Fonts/Arial");
            playlistFont = worldLoader.Load<SpriteFont>("Fonts/Miramonte");
            blank = new Texture2D(graphics.GraphicsDevice, 1, 1);
            blank.SetData(new[] { Color.White });

            // Load 3D game content <Models>
            ConfigReader.ModelsXML[] modelsXML = worldLoader.Load<ConfigReader.ModelsXML[]>("Models/Models");
            foreach (ModelsXML entry in modelsXML)
            {
                if (entry.Name == "Ground")
                {
                    modelHook = Load3DModels(entry);
                    break;
                }
            }
#if false
            // Create an animation player, and start decoding an animation clip.
            // NB: Required "Main" animation name
            modelManager.SkinModel["YukiBody"].Animation = animationContainer;
            modelManager.SkinModel["YukiBody"].Animation.Add("Main", skinningData, clip["Idle"], AnimationPlayer.PlaybackMode.LoopForward);
            modelManager.SkinModel["YukiBody"].Animation.Players["Main"].PlayerInfo.Interpolation = AnimationPlayer.InterpolationMode.Spherical;
            modelManager.SkinModel["YukiBody"].Animation.Play();
            // Create a disabled auxiliary player that plays idle one-time
            //animationContainer.Add("Aux", skinningData, clip[3], AnimationPlayer.PlaybackMode.Forward);
            //animationContainer.Players["Aux"].IsEnabled = false;
            //animationContainer.Play();            

            // Load the bounding spheres for all applicable skinned models
//            modelManager.AddCollisionSphere(modelManager.SkinModel["YukiBody"], "Models/Yuki/CollisionSpheres");

            // Create morph target and add morphs
            ...
            // Customize morph animations
            ...
            // create a viseme morph list
//            for (int i = 0; i < vFiles.Length; i++)
//            {
//                mi = Content.Load<MorphInfo>(vFiles[i]);
//                morphTarget.AddMorph(mi);
//            }
            // create a dynamic playlist: morphs will be added in real time, like speech for example
            //morphTarget.CreatePlayList("Viseme");

//            morphTarget.CreatePlayList("Test");
//            morphTarget.AddList("Smile", "Test");
//            morphTarget.AddList("BrowsUp", "Test");
//            for (int i = 0; i < vFiles.Length; i++)
//                morphTarget.AddList("V" + (i + 1).ToString("D2"), "Test");

//            for (int i = 0; i < morphTarget.Sequence["Test"].Count; i++)
//            {
//                morphTarget.Sequence["Test"].ElementAt(i).Duration = new TimeSpan(0, 0, 2);
//                morphTarget.Sequence["Test"].ElementAt(i).ForwardTime = new TimeSpan(0, 0, 0, 0, 1000);
//                morphTarget.Sequence["Test"].ElementAt(i).BackwardTime = new TimeSpan(0, 0, 0, 0, 1000);
//            }
#endif
        }


        private String Load3DModels(ModelsXML entry)
        {
            return Load3DModels(entry, false);
        }

        private String Load3DModels(ModelsXML entry, bool ignoreNoLoad)
        {
            String hook = String.Empty;
            int count = 0;

            ConfigReader.ConfigXML[] configXML = worldLoader.Load<ConfigReader.ConfigXML[]>("Models/" + entry.Path + "/Config");
            foreach (ConfigReader.ConfigXML data in configXML)
            {
                // Update progress
                controlPanel.UpdateProgress("Loading " + data.ID, (float)(++count * 1.0f / configXML.Length ));

                // Check if already loaded or not to load
                if (((data.NoLoad) && !(ignoreNoLoad)) ||
                    ((data.Type == ConfigXML.MType.SKINNED) && (modelManager.SkinModel.ContainsKey(data.ID))) ||
                    ((data.Type == ConfigXML.MType.STATIC) && (modelManager.StaticModel.ContainsKey(data.ID))) ||
                    // Also handle hair in a special way; ignoreNoLoad doubles as the indicator for normal/advanced type
                    // if false, normal hair is loaded; if true, advanced hair is loaded
                    // assumes no hair has been loaded yet at this point
                    ((data.ID.Contains("Hair")) && ((ignoreNoLoad) && (data.Name.Contains("-hra")))))
                    continue;
                
                // check hook
                if (String.IsNullOrEmpty(hook)) hook = entry.Name;

                // Load model
                modelManager.Add(data.ID, "Models/" + entry.Path + "/" + data.Name, (ModelsComponent.ModelType)data.Type);
                if (!String.IsNullOrEmpty(data.Parent))
                    if (modelManager.SkinModel.ContainsKey(data.ID))
                        modelManager.SkinModel[data.ID].Parent = data.Parent;
                    else if (modelManager.StaticModel.ContainsKey(data.ID))
                        modelManager.StaticModel[data.ID].Parent = data.Parent;
                if (!String.IsNullOrEmpty(data.AttachTo))
                    if (modelManager.SkinModel.ContainsKey(data.ID))
                        modelManager.SkinModel[data.ID].AttachTo = data.AttachTo;
                    else if (modelManager.StaticModel.ContainsKey(data.ID))
                        modelManager.StaticModel[data.ID].AttachTo = data.AttachTo;

                // Get Animation clips from skinningData
                if (data.Animation != null)
                {
                    SkinningData skinningData = modelManager.SkinModel[data.ID].TagInfo;
                    if (skinningData.AnimationClips.Count > 0)
                    {
                        List<SkinnedModel.AnimationInfo> clipList = new List<AnimationInfo>(skinningData.AnimationClips.Count);
                        for (int i = 0; i < data.Animation.Count; i++)
                        {
                            SkinnedModel.AnimationInfo aInfo = new SkinnedModel.AnimationInfo();
                            aInfo.animationName = data.Animation[i].Name;
                            aInfo.animationClip = skinningData.AnimationClips[data.Animation[i].Name];
                            aInfo.scale = (data.Animation[i].Scale != null) ? (float)data.Animation[i].Scale : 1.0f;
                            aInfo.next = (data.Animation[i].Next != null) ? (int)data.Animation[i].Next - i : 1;
                            aInfo.repeat = (data.Animation[i].Repeat != null) ? (int)data.Animation[i].Repeat : 1;
                            clipList.Add(aInfo);
                        }

                        // Create an animation player
                        // This loads the default animation in model file. Normally this is just T-pose or idle
                        // NB: Required "Main" animation name for default animation
                        AnimationContainer animationContainer = modelManager.SkinModel[data.ID].Animation;
                        animationContainer.ClipList = clipList;
                        animationContainer.defClipListLength = clipList.Count;
                        animationContainer.animationHandler += new AnimationHandler(animationHandler);
                        if (!String.IsNullOrEmpty(data.DefAnimation))
                        {
                            AnimationInfo aInfo = new AnimationInfo();
                            foreach (AnimationInfo a in clipList) {
                                if (a.animationName == data.DefAnimation) {
                                    aInfo = a;
                                    break;
                                }
                            }
                            animationContainer.Add("Main", 
                                                    skinningData, 
                                                    data.DefAnimation, 
                                                    aInfo.animationClip, 
                                                    (aInfo.repeat == 0) ? AnimationPlayer.PlaybackMode.LoopForward : AnimationPlayer.PlaybackMode.Forward);
                            animationContainer.MainPlayer.Scale = 2.4f / (0.8f * aInfo.scale); // scales from 24fps to 10fps for scale=1
                            animationContainer.MainPlayer.PlayerInfo.Interpolation = AnimationPlayer.InterpolationMode.Cubic;
                            animationContainer.MainPlayer.Repeat = aInfo.repeat;
                            animationContainer.Play();
                        }
                    }
                    else
                    {
                        // no embedded animation clip found in model. just create an animation player with blank clip
                        AnimationContainer animationContainer = modelManager.SkinModel[data.ID].Animation;
                        animationContainer.Add("Main", skinningData, String.Empty, null, AnimationPlayer.PlaybackMode.Forward); // last argument does not matter
                    }
                }

                // Check mesh visibility
                if (data.NotVisible != null)
                    foreach (String meshName in data.NotVisible)
                        modelManager.ChangeMeshVisibilityTag(data.ID, meshName, false);

                // Position model
                if (data.InitialData != null)
                    modelManager.InitTransform(data.ID, data.InitialData);

                // Change draw order
                if (data.DrawOrder != null)
                    modelManager.ChangeModelOrder(data.ID, (short)data.DrawOrder);

                // Import morphs and animations
                if (data.Import != null)
                {
                    if (data.Import.Morphs != null)
                    {
                        foreach (String m in data.Import.Morphs)
                            modelManager.AddMorphTarget(data.ID, "Models/" + entry.Path + "/Morphs/" + m);
                    }
                    if (data.Import.Skeletal != null)
                    {
                    }
                    if (data.Import.Visemes != null)
                    {
                        foreach (String v in data.Import.Visemes)
                            modelManager.AddMorphTarget(data.ID, "Models/" + entry.Path + "/Visemes/" + v);
                        if (data.Import.VisemeMap != null)
                            modelManager.AddVisemeMap(data.ID, data.Import.VisemeMap.ToArray<int>());
                    }
                    if (data.Import.KnowledgeBase != null)
                    {
                        modelManager.AddPersonality(entry.Path, data.ID, data.Import.KnowledgeBase.ToArray<string>());
                    }
                }

                // Add SoftBody Spring physics: physics is only supported for skinned models
                if (data.Physics != null)
                    foreach (ConfigReader.Physics p in data.Physics)
                        modelManager.SkinModel[data.ID].AddSoftBodyDynamics(p);
            }
            // Update progress
            controlPanel.UpdateProgress("Done.", 1.0f);
            // return model hook reference name
            return hook;
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Demo: Morph animation control
            // just for fun, randomize blinking everytime between 5 to 10s
            if (modelManager.SkinModel.ContainsKey(modelHook + "Head"))
            {
                int rand = new Random().Next(5, 10);
                foreach (CMorphTarget mt in modelManager.SkinModel[modelHook + "Head"].MorphTargets.Values)
                {
                    if ((mt.Players.ContainsKey("Blink")) && (mt.Players["Blink"].State == MorphPlayer.state.HOLD))
                        mt.Players["Blink"].RepeatTime = new TimeSpan(0, 0, rand);
                }
            }

            // Screen effects
            if (bScreenfade)
            {
                if (modelChangedItem.Type != changedEnum.None)
                    ModelChangedHandler();  // do this only once

                alpha -= (int)(100 * gameTime.ElapsedGameTime.TotalSeconds);
                alpha = (int)MathHelper.Clamp(alpha, 0.0f, 255.0f);
                if (alpha == 0)
                {
                    bScreencap = false;
                    bScreenfade = false;
                }
            }
            
            // Demo: Procedural bone control
//            UpdateHeadMovement(gameTime, modelManager.SkinModel["YukiBody"]);

            // quit if commanded on chat
            try
            {
                if (modelManager.SkinModel[modelHook + "Head"].Personality.Quitting)
                    if (avatarVoice.Status.RunningState == SpeechRunState.SRSEDone)
                        this.Exit();
            }
            catch { }
               
            base.Update(gameTime);
        }

        #region Callbacks and Handlers

#if false
        /// <summary>
        /// Game specific demo: Procedural Bone control
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values</param>
        /// <param name="mdl">Skinned model to update head movement</param>
        private void UpdateHeadMovement(GameTime gameTime, CSkinnedModel mdl)
        {
            // head moves accdg to camera rotation and arc
            // not totally accurate, but at least the head follows

            //Neck limits: bend=(-50,50), twist=(-50,50), side-side=(-40,40)
            //Head limits: bend=(-40,40), twist=(-80,80), side-side=(-10,10)

            int neckIndex = mdl.TagInfo.BoneIndices["neck"];
            int headIndex = mdl.TagInfo.BoneIndices["head"];
            float delta = 0.5f;

            Vector3 camPos = Vector3.Zero;
            camPos.X = (float)Math.Sin(MathHelper.ToRadians(camera["Main"].Rotation));
            camPos.Z = (float)Math.Cos(MathHelper.ToRadians(camera["Main"].Rotation));
            camPos.Y = -(float)Math.Sin(MathHelper.ToRadians(camera["Main"].Arc));
            camPos = Vector3.Normalize(camPos);

            float targetRot = (headFollow == true) ? MathHelper.ToDegrees((float)Math.Atan2(camPos.X, camPos.Z)) : 0.0f;
            float targetArc = (headFollow == true) ? MathHelper.ToDegrees(-(float)Math.Atan2(camPos.Y, camPos.Z)) : 0.0f;

            if (targetRot - delta > headRot)
                headRot += (targetRot - headRot) * 0.1f;
            else if (targetRot + delta < headRot)
                headRot -= (headRot - targetRot) * 0.1f;
            if (targetArc - delta > headArc)
                headArc += (targetArc - headArc) * 0.1f;
            else if (targetArc + delta < headArc)
                headArc -= (headArc - targetArc) * 0.1f;

            headRot = MathHelper.Clamp(headRot, -75.0f, +75.0f);
            headArc = MathHelper.Clamp(headArc, -30.0f, +60.0f);
            float neckArc = MathHelper.Clamp(headArc / 2, -3.0f, 5.0f);

            mdl.Animation.Players["Main"].PlayerInfo.ProceduralTransforms[headIndex] =
                    Matrix.CreateRotationZ(MathHelper.ToRadians(2 * headRot / 3)) *
                    Matrix.CreateRotationX(MathHelper.ToRadians(headArc - neckArc));
            mdl.Animation.Players["Main"].PlayerInfo.ProceduralTransforms[neckIndex] =
                    Matrix.CreateRotationZ(MathHelper.ToRadians(headRot / 3)) *
                    Matrix.CreateRotationX(MathHelper.ToRadians(neckArc));
        }
#endif
#if false
        /// <summary>
        /// Event Handler for mouse move/drag
        /// </summary>
        /// <param name="args">Mouse event parameter</param>
        void MouseMoveFunction(MouseEventArgs args, GameTime gameTime)
        {
            if (controlPanel.InputFocused) return;

            // drags model around in view XY
            if (args.State.LeftButton == ButtonState.Pressed)
            {
                Rectangle clientBounds = this.Window.ClientBounds;
                Vector2 mouse;

                int centerX = clientBounds.Width / 2;
                int centerY = clientBounds.Height / 2;
                mouse.X = (float)centerX - args.Position.X;
                mouse.Y = (float)centerY - args.Position.Y;
                Mouse.SetPosition(centerX, centerY);

                /**/Vector3 modelTrans = selectedModel.Translation;
                /**/float modelRot = selectedModel.Rotation.Y;

                // translate model in the horizontal/vertical with respect to current view
                // so even if model is rotated, it appears we're moving along the view plane
                // we'll use cameraRotation, cameraArc and modelRot for this.
                modelTrans.X += mouse.X * 0.5f * (float)Math.Cos(MathHelper.ToRadians(-camera["Main"].Rotation - modelRot)) +
                                 mouse.Y * 0.5f * (float)Math.Sin(MathHelper.ToRadians(-camera["Main"].Arc)) * (float)Math.Sin(MathHelper.ToRadians(-camera["Main"].Rotation - modelRot));
                modelTrans.Z += mouse.X * 0.5f * (float)Math.Sin(MathHelper.ToRadians(-camera["Main"].Rotation - modelRot)) +
                                -mouse.Y * 0.5f * (float)Math.Sin(MathHelper.ToRadians(-camera["Main"].Arc)) * (float)Math.Cos(MathHelper.ToRadians(-camera["Main"].Rotation - modelRot));
                modelTrans.Y += mouse.Y * 0.5f * (float)Math.Cos(MathHelper.ToRadians(-camera["Main"].Arc));

                //modelManager.SkinModel["YukiBody"].Animation.modelTransform =
                //    Matrix.CreateRotationY(MathHelper.ToRadians(modelRot)) *
                //    Matrix.CreateTranslation(modelTrans);

                /**/selectedModel.Translation = modelTrans;

                //camera1.HandlerUpdate = true;
                camera["Sub"].HandlerUpdate = true;
            }

        }

        /// <summary>
        /// Event Handler for mouse down button click
        /// </summary>
        /// <param name="args">Mouse event parameter</param>
        private void MouseDownFunction(MouseEventArgs args, GameTime time)
        {
            if (controlPanel.InputFocused) return;

            if ((args.Button == MouseButtons.Left) || (args.Button == MouseButtons.Right))
            {
                Rectangle clientBounds = this.Window.ClientBounds;
                int centerX = clientBounds.Width / 2;
                int centerY = clientBounds.Height / 2;
                Mouse.SetPosition(centerX, centerY);
                this.IsMouseVisible = false;
            }
        }

        /// <summary>
        /// Event Handler for mouse up button click
        /// </summary>
        /// <param name="args"></param>
        void MouseUpFunction(MouseEventArgs args, GameTime time)
        {
            if (controlPanel.InputFocused) return;
            
            if (args.Button == MouseButtons.Right)
            {
                this.IsMouseVisible = true;
                camera["Main"].HandlerUpdate = true;
            }

            // restore model position, no point in leaving her hanging around in space
            // if terrain isn't flat, add terrainY to offset
            if (args.Button == MouseButtons.Left)
            {
                /**/Vector3 modelTrans = selectedModel.Translation;
                modelTrans.Y = 0.0f;
                //modelManager.SkinModel["YukiBody"].Animation.modelTransform =
                //    Matrix.CreateRotationY(MathHelper.ToRadians(modelRot)) *
                //    Matrix.CreateTranslation(modelTrans);
                this.IsMouseVisible = true;
                camera["Main"].HandlerUpdate = true;
                camera["Sub"].HandlerUpdate = true;

                /**/selectedModel.Translation = modelTrans;
            }
        }
#endif
        /// <summary>
        /// Event Handler for key press
        /// </summary>
        /// <param name="args">Key event parameter</param>
        private void KeyDownFunction(KeyEventArgs args, GameTime gameTime)
        {
            if (controlPanel.InputFocused) return;            
            
            float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // check for exit
            if (args.Key == Keys.Escape)
            {
                if (avatarVoice != null)
                    avatarVoice.Speak("Are you sure?", SpeechVoiceSpeakFlags.SVSFlagsAsync);
                if (Forms.Helpers.MessageBox("Are you sure?", "Quit EskNA", System.Windows.Forms.MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
                    Exit();
            }

            // toggles bounding spheres display
            if (args.Key == Keys.OemOpenBrackets)
                modelManager.showSpheres = !modelManager.showSpheres;
            // toggles sub camera enable 
            if (args.Key == Keys.OemCloseBrackets)
                camera["Sub"].Enabled = !camera["Sub"].Enabled;
            // toggles head follow movement
            //if (args.Key == Keys.H)
            //    headFollow = !headFollow;
            // toggles debug
            if (args.Key == Keys.G)
                _DEBUG_ = !_DEBUG_;
#if false
            if (args.Key == Keys.T)
            {
                debugMsg = "";
                morphTarget.StartPlayList("Test", false);
            }
#endif
            // move model around: brute force method
#if false            
            Vector3 modelPos = Vector3.Zero;
            /**/Vector3 modelTrans = selectedModel.Translation;
            /**/float modelRot = selectedModel.Rotation.Y;

            if ((args.Key == Keys.Left) || (args.Key == Keys.A))
                modelRot -= time * 0.2f;
            if ((args.Key == Keys.Right) || (args.Key == Keys.D))
                modelRot += time * 0.2f;
            //modelRot = MathHelper.ToDegrees(MathHelper.WrapAngle(MathHelper.ToRadians(modelRot)));
            modelPos.X = (float)Math.Sin(MathHelper.ToRadians(modelRot));
            modelPos.Z = (float)Math.Cos(MathHelper.ToRadians(modelRot));
            if ((args.Key == Keys.Up) || (args.Key == Keys.W))
                modelTrans += Vector3.Multiply(modelPos, 20.0f);
            if ((args.Key == Keys.Down) || (args.Key == Keys.S))
                modelTrans -= Vector3.Multiply(modelPos, 20.0f);

            /**/selectedModel.Rotation = new Vector3(0.0f, modelRot, 0.0f);
            /**/selectedModel.Translation = modelTrans;
#endif
        }

        /// <summary>
        /// Handles main camera update
        /// <param name="gameTime">Timing values snapshot</param>
        /// <param name="key">Key being pressed</param>
        /// <param name="mouse">Mouse status</param>
        /// </summary>
        private void Main_cameraHandler(GameTime gameTime, KeyEventArgs key, MouseEventArgs mouse)
        {
            //if (controlPanel.InputFocused) return;
            
            float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            // update keys
            if (key.Key == Keys.OemPipe)
                camera["Main"].SpringEnabled = !camera["Main"].SpringEnabled;

            // update mouse
            // rotates camera
           // if (mouse.Button == MouseButtons.Left)
           // {
           //     camera["Main"].Rotation += mouse.Position.X * time * 0.05f;
           //     camera["Main"].Arc += mouse.Position.Y * time * 0.05f;
           //     camera["Main"].Arc = MathHelper.Clamp(camera["Main"].Arc, -90.0f, 90.0f);
           // }
            // changes camera vertical and horizontal movement
            if (mouse.Button == MouseButtons.Middle)
            {
                Vector3 offset = camera["Main"].LookAtOffset;
                Vector3 position = camera["Main"].DesiredPositionOffset;
                offset.X -= mouse.Position.X * 0.1f;
                offset.Y += mouse.Position.Y * 0.1f;
                position.X -= mouse.Position.X * 0.1f;
                position.Y += mouse.Position.Y * 0.1f;

                camera["Main"].LookAtOffset = offset;
                camera["Main"].DesiredPositionOffset = position;
            }
            // changes camera focus and vertical movement
            if (mouse.Button == MouseButtons.Right)
            {
                camera["Main"].Rotation += mouse.Position.X * time * 0.05f;
                camera["Main"].Arc += mouse.Position.Y * time * 0.05f;
                camera["Main"].Arc = MathHelper.Clamp(camera["Main"].Arc, -90.0f, 90.0f);
                //Vector3 offset = camera["Main"].LookAtOffset;
                //Vector3 position = camera["Main"].DesiredPositionOffset;
                //offset.Y -= mouse.Position.Y * 0.1f;
                //position.Y -= mouse.Position.Y * 0.1f;
                //position.Z -= mouse.Position.X * 0.1f;
                //
                //camera["Main"].LookAtOffset = offset;
                //camera["Main"].DesiredPositionOffset = position;
            }
            // changes camera zoom
            if (mouse.Button == MouseButtons.None)
            {
                Vector3 position = camera["Main"].DesiredPositionOffset;
                position.Z += mouse.Scroll * 0.05f;
                camera["Main"].DesiredPositionOffset = position;
            }

            // update chase camera
            if ((camera["Main"].ChaseName != null) && (modelManager.SkinModel.ContainsKey(camera["Main"].ChaseName) ||
                                                       modelManager.StaticModel.ContainsKey(camera["Main"].ChaseName)))
            {
                if (modelManager.SkinModel.ContainsKey(camera["Main"].ChaseName))
                    camera["Main"].ChasePosition = modelManager.SkinModel[camera["Main"].ChaseName].Translation;
                else
                    camera["Main"].ChasePosition = modelManager.StaticModel[camera["Main"].ChaseName].Translation;

                Vector3 modelPos = Vector3.Zero;
                /**/ float modelRot = 0.0f;
                if (modelManager.SkinModel.ContainsKey(modelHook + "BodyA"))
                    modelRot = modelManager.SkinModel[modelHook + "BodyA"].Rotation.Y;
                if (modelManager.StaticModel.ContainsKey(modelHook))
                    modelRot = modelManager.StaticModel[modelHook].Rotation.Y;

                modelPos.X = (float)Math.Sin(MathHelper.ToRadians(modelRot + camera["Main"].Rotation));
                modelPos.Z = (float)Math.Cos(MathHelper.ToRadians(modelRot + camera["Main"].Rotation));
                modelPos.Y = -(float)Math.Sin(MathHelper.ToRadians(camera["Main"].Arc));
                camera["Main"].ChaseDirection = Vector3.Normalize(modelPos);

                camera["Main"].ChaseUp = Vector3.Up;
            }
            else
            {
                // no model to chase
                camera["Main"].ChasePosition = Vector3.Zero;
                camera["Main"].ChaseDirection = Vector3.Zero;
                camera["Main"].ChaseUp = Vector3.Up;
            }

            camera["Main"].HandlerUpdate = false;
        }

        /// <summary>
        /// Handles sub camera update
        /// <param name="gameTime">Timing values snapshot</param>
        /// <param name="key">Key being pressed</param>
        /// <param name="mouse">Mouse status</param>
        /// </summary>
        void Sub_cameraHandler(GameTime gameTime, KeyEventArgs key, MouseEventArgs mouse)
        {
            //if (controlPanel.InputFocused) return;
            
            float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // update chase camera
            if ((camera["Sub"].ChaseName != null) && (modelManager.SkinModel.ContainsKey(camera["Sub"].ChaseName) ||
                                                      modelManager.StaticModel.ContainsKey(camera["Sub"].ChaseName)))
            {
                if (modelManager.SkinModel.ContainsKey(camera["Sub"].ChaseName))
                    camera["Sub"].ChasePosition = modelManager.SkinModel[camera["Sub"].ChaseName].Translation;
                else
                    camera["Sub"].ChasePosition = modelManager.StaticModel[camera["Sub"].ChaseName].Translation;

                Vector3 modelPos = Vector3.Zero;
                /**/
                float modelRot = 0.0f;
                if (modelManager.SkinModel.ContainsKey(modelHook + "BodyA"))
                    modelRot = modelManager.SkinModel[modelHook + "BodyA"].Rotation.Y;
                if (modelManager.StaticModel.ContainsKey(modelHook))
                    modelRot = modelManager.StaticModel[modelHook].Rotation.Y;

                modelPos.X = (float)Math.Sin(MathHelper.ToRadians(modelRot));
                modelPos.Z = (float)Math.Cos(MathHelper.ToRadians(modelRot));
                camera["Sub"].ChaseDirection = Vector3.Normalize(modelPos);

                camera["Sub"].ChaseUp = Vector3.Up;
            }
            camera["Sub"].HandlerUpdate = false;
        }


        /// <summary>
        /// Event handler for animation status events
        /// </summary>
        /// <param name="playerName">ID of animation player</param>
        void animationHandler(string modelID, string playerName, AnimationPlayer.PlaybackStatus status)
        {
            AnimationContainer aContainer = modelManager.SkinModel[modelID].Animation;

            if (playerName == "Main")
            {
                if (status == AnimationPlayer.PlaybackStatus.End)
                {
                    if ((MediaPlayer.State != MediaState.Playing) && (!aContainer.MainPlayer.AnimationClipName.Contains("Dance")))
                    {
                        // proceed with next clip
                        for (int i = 0; i < aContainer.ClipList.Count; i++)
                        {
                            if (aContainer.ClipList[i].animationName == aContainer.MainPlayer.AnimationClipName)
                            {
                                int nextClip = aContainer.ClipList[i].next + i;
                                aContainer.MainPlayer.AnimationClips = aContainer.ClipList[nextClip].animationClip;
                                aContainer.MainPlayer.AnimationClipName = aContainer.ClipList[nextClip].animationName;
                                aContainer.MainPlayer.Scale = 2.4f / (0.8f * aContainer.ClipList[nextClip].scale);
                                aContainer.MainPlayer.PlaybackMode = (aContainer.ClipList[nextClip].repeat == 0) ? AnimationPlayer.PlaybackMode.LoopForward : AnimationPlayer.PlaybackMode.Forward;
                                aContainer.MainPlayer.Repeat = aContainer.ClipList[nextClip].repeat;
                                aContainer.Play();
                                break;
                            }
                        }
                    }
                    else
                    {
                        // load next dance step
                        bScreencap = true;
                        modelChangedItem.Type = changedEnum.Dance;
                    }
                }
            }

#if false
            // TODO: debug: why always main-end?
            if (playerName == "Main")
            {
                if (status == AnimationPlayer.PlaybackStatus.End)
                {
                    //otherMsg += "\nmain-idle-forward";

                    if ((clipIndex != 0) && (animationContainer.Players["Aux"].PlaybackMode == AnimationPlayer.PlaybackMode.Forward))
                    {
                        // Main-idle-forward has finished, start an Aux-transition backward-forward
                        animationContainer.Players["Aux"].AnimationClips = clip[clipIndex];
                        animationContainer.Players["Aux"].PlaybackMode = AnimationPlayer.PlaybackMode.Backward;
                        animationContainer.Players["Aux"].FadeDuration = TimeSpan.FromSeconds(0.5f);
                        animationContainer.Players["Aux"].IsEnabled = true;
                        animationContainer.Play();
                    }
                    else if ((clipIndex != 0) && (animationContainer.Players["Aux"].PlaybackMode == AnimationPlayer.PlaybackMode.Backward))
                    {
                        // Main-transition-backward has finished, return to idle
                        animationContainer.Players["Main"].AnimationClips = clip[0];
                        animationContainer.Players["Main"].PlaybackMode = AnimationPlayer.PlaybackMode.LoopForward;
                        animationContainer.Players["Main"].FadeDuration = TimeSpan.FromSeconds(0.5f);
                        animationContainer.Players["Aux"].IsEnabled = false;
                        animationContainer.Play();

                        // reset clip and idle timer
                        clipIndex = 0;
                        idleTimer = TimeSpan.Zero;
                    }
                }
            }
            if (playerName == "Aux")
            {
                if (status == AnimationPlayer.PlaybackStatus.End)
                {
                    if (mode == AnimationPlayer.PlaybackMode.Forward)
                    {
                        //otherMsg += "\naux-transition-forward";
                    }
                    else if (mode == AnimationPlayer.PlaybackMode.Backward)
                    {
                        //otherMsg += "\naux-transition-backward";
                        animationContainer.Players["Aux"].IsEnabled = false;
                    }
                }
            }
#endif
        }

        /// <summary>
        /// Sequence Handler callback
        /// Called when an event is triggered by morph sequence player
        /// </summary>
        /// <param name="sname">Name of sequence currently being played</param>
        /// <param name="mname">Name of active morph</param>
        /// <param name="status">Play status (START, NEXT, DONE)</param>
        private void morphTarget_sHandler(string sname, string mname, PlayStatus status)
        {
#if false
            if (sname == "Test")
            {
                if (status == PlayStatus.DONE)
                    morphTarget.ResetPlayList(sname);
                
                debugMsg += mname + ": " + status + "\n";
            }
#endif
#if false
            if (sname == "Test")
            {
                if (status == PlayStatus.DONE)
                {
                    morphTarget.ResetPlayList(sname);
                }
                else
                {
                    if (mname == "E01") otherMsg += "Happy\n";
                    if (mname == "E02") otherMsg += "Disgusted\n";
                    if (mname == "E03") otherMsg += "Sad\n";
                    if (mname == "E04") otherMsg += "Afraid\n";
                    if (mname == "E05") otherMsg += "Angry\n";
                    if (mname == "E06") otherMsg += "Surprised\n";
                    //otherMsg += mname + " " /*+ status.ToString()*/ + "\n";
                }
            }
            if (sname == "Viseme")
            {
                // a DONE status is not particularly useful in dynamic sequence since
                // handler may report end of sequence but a new morph may be added still
                // Changed: DONE status will be generated only on end of sequence iff
                //  sequence addition was handled properly

                // only use this callback if we need to do something per viseme             

                //otherMsg += mname + " " /*+ status.ToString()*/ /*+ "\n"*/;
                //otherMsg += status.ToString() + "\n";
                if (status == PlayStatus.DONE)
                {
                    morphTarget.ResetPlayList(sname);
                }
            }
#endif
        }


        #endregion

        #region Graphics

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            bool rtdone = false;
            if ((bScreencap) && (rt == null))
            {
                rt = new RenderTarget2D(graphics.GraphicsDevice, this.Window.ClientBounds.Width, this.Window.ClientBounds.Height,
                                        false, SurfaceFormat.Color, DepthFormat.Depth24);
                graphics.GraphicsDevice.SetRenderTarget(rt);
                alpha = 256;
                rtdone = true;
            }
            else if (!bScreencap)
            {
                if (rt!=null) rt.Dispose();
                rt = null;
            }

            //GraphicsDevice.Clear(Color.CornflowerBlue);
            graphics.GraphicsDevice.Clear(Color.Black);

            //graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
            graphics.GraphicsDevice.BlendState = BlendState.AlphaBlend;
            graphics.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            graphics.GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;

            RasterizerState rasterizerState1 = new RasterizerState();
            rasterizerState1.CullMode = CullMode.CullCounterClockwiseFace;
            rasterizerState1.MultiSampleAntiAlias = true;
            graphics.GraphicsDevice.RasterizerState = rasterizerState1;
            graphics.GraphicsDevice.PresentationParameters.MultiSampleCount = 2;

            base.Draw(gameTime);

            if ((bScreencap) && (rtdone == true))
            {
                graphics.GraphicsDevice.SetRenderTarget(null);
                //base.Draw(gameTime);
                rtdone = false;
            }

#if false
            if (_DEBUG_)
            {
                RasterizerState dState = new RasterizerState();
                dState.FillMode = FillMode.WireFrame;
                GraphicsDevice.RasterizerState = dState;

                int j = 0;
                /**
                foreach (Physics.SoftBodyDynamics psx in modelManager.SkinModel["EskHair"].SoftBodyDynamics.Values)
                {
                    for (int i = 0; i < psx.SoftBodyObjects.Count; i++)
                    {
                        Vector3 dCenter = psx.SoftBodyObjects[i].CurrPosition;
                        Quaternion dRotate = psx.SoftBodyObjects[i].CurrRotation;
                        Matrix dWorld = Matrix.CreateScale(1.0f) *
                                        Matrix.CreateFromQuaternion(dRotate) *
                                        Matrix.CreateTranslation(dCenter);
                        Color col = Color.Red;
                        if (i == 0) col = Color.Blue;
                        dSphere[j++].Draw(dWorld, camera["Main"].ViewMatrix, camera["Main"].ProjectionMatrix, col);

                        // trying to draw a line using same device, to no success, use small sphere instead
                        dWorld = Matrix.CreateScale(0.2f) *
                                 Matrix.CreateTranslation(dCenter + dWorld.Right * 0.5f);
                        dSphere[j++].Draw(dWorld, camera["Main"].ViewMatrix, camera["Main"].ProjectionMatrix, Color.White);
                    }
                }
                /**/
                foreach (Physics.SoftBodyDynamics psx in modelManager.SkinModel["EskBodyA"].SoftBodyDynamics.Values)
                {
                    for (int i = 0; i < psx.SoftBodyObjects.Count; i++)
                    {
                        Vector3 dCenter = psx.SoftBodyObjects[i].CurrPosition;
                        Quaternion dRotate = psx.SoftBodyObjects[i].CurrRotation;
                        Matrix dWorld = Matrix.CreateScale(1.0f) *
                                        Matrix.CreateFromQuaternion(dRotate) *
                                        Matrix.CreateTranslation(dCenter);
                        Color col = Color.Red;
                        if (i < 2) col = Color.Blue;
                        if (i >= psx.SoftBodyObjects.Count - 2) col = Color.Blue;
                        dSphere[j++].Draw(dWorld, camera["Main"].ViewMatrix, camera["Main"].ProjectionMatrix, col);

                        // trying to draw a line using same device, to no success, use small sphere instead
                        dWorld = Matrix.CreateScale(0.2f) *
                                 Matrix.CreateTranslation(dCenter + dWorld.Backward * 0.5f);
                        dSphere[j++].Draw(dWorld, camera["Main"].ViewMatrix, camera["Main"].ProjectionMatrix, Color.White);
                    }
                }
                /**/

                GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            }
#endif
            //fps = 1000.0f / (float)gameTime.ElapsedGameTime.Milliseconds;
            accumElapsedTimeSec += (float)gameTime.ElapsedGameTime.TotalSeconds;
            frames++;
            if (accumElapsedTimeSec >= 1.0f)
            {
                fps = frames;
                frames = 0;
                accumElapsedTimeSec = 0.0;
            }

            if (camera["Sub"].Enabled)
                DrawViewportEdges(camera["Sub"].Viewport);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
            if (bScreencap)
            {
                bScreenfade = true;
                spriteBatch.Draw(rt, new Rectangle(0, 0, this.Window.ClientBounds.Width, this.Window.ClientBounds.Height),
                             new Color(255, 255, 255, alpha));
            }
            debugMsg = "_DEBUG_: " + _DEBUG_.ToString() + Environment.NewLine ;
            debugMsg += "_DEBUG_: alpha=" + alpha.ToString() + Environment.NewLine;
            debugMsg += "_DEBUG_: moves=" + danceIndex.ToString();
            spriteBatch.DrawString(spriteFont, this.Window.Title + "\n" + message + fps.ToString() + "\n\n" + debugMsg, 
                new Vector2(0.0f, 0.0f), Color.Yellow);
            for (int i = 0; i < songList.Count; i++)
            {
                if ((i == 0) && (MediaPlayer.State != MediaState.Stopped))
                {
                    spriteBatch.DrawString(playlistFont, songList[i].Name, 
                                           new Vector2(this.Window.ClientBounds.Width-190, i * 12), Color.Cyan);
                    spriteBatch.DrawString(playlistFont, GetHumanReadableTime(MediaPlayer.PlayPosition),
                                           new Vector2(this.Window.ClientBounds.Width - 40, i * 12), Color.Cyan);
                }
                else
                    spriteBatch.DrawString(playlistFont, songList[i].Name,
                                           new Vector2(this.Window.ClientBounds.Width - 190, i * 12), Color.White);
            }
            spriteBatch.End();
        }

        /// <summary>
        /// A helper to draw the edges of a viewport.
        /// </summary>
        private void DrawViewportEdges(Viewport viewport)
        {
            const int edgeWidth = 2;

            // We now compute four rectangles that make up our edges
            Rectangle topEdge = new Rectangle(
                viewport.X - edgeWidth / 2,
                viewport.Y - edgeWidth / 2,
                viewport.Width + edgeWidth,
                edgeWidth);
            Rectangle bottomEdge = new Rectangle(
                viewport.X - edgeWidth / 2,
                viewport.Y + viewport.Height - edgeWidth / 2,
                viewport.Width + edgeWidth,
                edgeWidth);
            Rectangle leftEdge = new Rectangle(
                viewport.X - edgeWidth / 2,
                viewport.Y - edgeWidth / 2,
                edgeWidth,
                viewport.Height + edgeWidth);
            Rectangle rightEdge = new Rectangle(
                viewport.X + viewport.Width - edgeWidth / 2,
                viewport.Y - edgeWidth / 2,
                edgeWidth,
                viewport.Height + edgeWidth);

            // We just use SpriteBatch to draw the four rectangles
            spriteBatch.Begin();
            spriteBatch.Draw(blank, topEdge, Color.White);
            spriteBatch.Draw(blank, bottomEdge, Color.White);
            spriteBatch.Draw(blank, leftEdge, Color.White);
            spriteBatch.Draw(blank, rightEdge, Color.White);
            spriteBatch.End();
        }

        #endregion


    }
}
